package org.hilo.boot.core.shiro;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.CacheManagerAware;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 使用 CacheManager 管理 Cache

处理流程：
<p> 
  readSession：首先会从 CacheManager 中查询ActiveSessions，根据sessionId取出Session
如果没有，则调用 readSession获取，该方法最终会调用 doReadSession获取，此方法可以扩展为从 Redis获取Session
<p> 
  create：首先调用父类的create方法，该方法最终会调用 doCreate方法创建Session，此方法可以扩展为在 Redis中创建Session
然后，会调用 cache方法，把Session缓存到CacheManager中。
<p> 
  update：首先调用doUpdate方法更新缓存，此方法可以扩展为在 Redis中更新Session，
然后cache或者uncache方法在CacheManager中覆盖更新或者删除失效Session。
<p> 
  delete：首先调用uncache方法在CacheManager中删除Session，然后调用doDelete方法删除缓存，此方法可以扩展为在 Redis中删除Session。
  
<p> 
  故，如果要实现 Redis管理缓存，可以实现 doReadSession、doCreate、doUpdate、doDelete 四个方法。

 * 
 * @author zollty
 * @since 2017-06-30
 */
public abstract class AbstractCacheSessionDao extends AbstractSessionDAO implements CacheManagerAware {
    private static Logger logger = LoggerFactory.getLogger(AbstractCacheSessionDao.class);

    /**
     * The default active sessions cache name, equal to {@code shiro-activeSessionCache}.
     */
    public static final String ACTIVE_SESSION_CACHE_NAME = "shiro-activeSessionCache";

    /**
     * The CacheManager to use to acquire the Session cache.
     */
    private CacheManager cacheManager;

    /**
     * The Cache instance responsible for caching Sessions.
     */
    private Cache<Serializable, Session> activeSessions;

    /**
     * The name of the session cache, defaults to {@link #ACTIVE_SESSION_CACHE_NAME}.
     */
    private String activeSessionsCacheName = ACTIVE_SESSION_CACHE_NAME;
    
    private Set<Serializable> weakHashSet = Collections.newSetFromMap(
            new WeakHashMap<Serializable, Boolean>());
    
    private boolean readFromRemote;

    /**
     * Default no-arg constructor.
     */
    public AbstractCacheSessionDao() {
    }

    /**
     * Sets the cacheManager to use for acquiring the {@link #getActiveSessionsCache() activeSessionsCache} if
     * one is not configured.
     *
     * @param cacheManager the manager to use for constructing the session cache.
     */
    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    /**
     * Returns the CacheManager to use for acquiring the {@link #getActiveSessionsCache() activeSessionsCache} if
     * one is not configured.  That is, the {@code CacheManager} will only be used if the
     * {@link #getActiveSessionsCache() activeSessionsCache} property is {@code null}.
     *
     * @return the CacheManager used by the implementation that creates the activeSessions Cache.
     */
    public CacheManager getCacheManager() {
        return cacheManager;
    }

    /**
     * Returns the name of the actives sessions cache to be returned by the {@code CacheManager}.  Unless
     * overridden by {@link #setActiveSessionsCacheName(String)}, defaults to {@link #ACTIVE_SESSION_CACHE_NAME}.
     *
     * @return the name of the active sessions cache.
     */
    public String getActiveSessionsCacheName() {
        return activeSessionsCacheName;
    }

    /**
     * Sets the name of the active sessions cache to be returned by the {@code CacheManager}.  Defaults to
     * {@link #ACTIVE_SESSION_CACHE_NAME}.
     *
     * @param activeSessionsCacheName the name of the active sessions cache to be returned by the {@code CacheManager}.
     */
    public void setActiveSessionsCacheName(String activeSessionsCacheName) {
        this.activeSessionsCacheName = activeSessionsCacheName;
    }

    /**
     * Returns the active sessions cache, but if that cache instance is null, first lazily creates the cache instance
     * via the {@link #createActiveSessionsCache()} method and then returns the instance.
     * <p/>
     * Note that this method will only return a non-null value code if the {@code CacheManager} has been set.  If
     * not set, there will be no cache.
     *
     * @return the active sessions cache instance.
     */
    private Cache<Serializable, Session> getActiveSessionsCacheLazy() {
        if (this.activeSessions == null) {
            this.activeSessions = createActiveSessionsCache();
        }
        return activeSessions;
    }

    /**
     * Creates a cache instance used to store active sessions.  Creation is done by first
     * {@link #getCacheManager() acquiring} the {@code CacheManager}.  If the cache manager is not null, the
     * cache returned is that resulting from the following call:
     * <pre>       String name = {@link #getActiveSessionsCacheName() getActiveSessionsCacheName()};
     * cacheManager.getCache(name);</pre>
     *
     * @return a cache instance used to store active sessions, or {@code null} if the {@code CacheManager} has
     *         not been set.
     */
    private Cache<Serializable, Session> createActiveSessionsCache() {
        Cache<Serializable, Session> cache = null;
        CacheManager mgr = getCacheManager();
        if (mgr != null) {
            String name = getActiveSessionsCacheName();
            cache = mgr.getCache(name);
        }
        return cache;
    }
    
    /**
     * 返回本机CacheManager中Session，不适合分布式环境几种缓存
     * Returns all active sessions in the system.
     * <p/>
     * <p>This implementation merely returns the sessions found in the activeSessions cache.  Subclass implementations
     * may wish to override this method to retrieve them in a different way, perhaps by an RDBMS query or by other
     * means.
     *
     * @return the sessions found in the activeSessions cache.
     */
    public Collection<Session> getCacheManagerActiveSessions() {
        // 获取CacheManager中的Cache，如果没有，则会创建一个空的Cache对象
        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
        if (cache != null) {
            return cache.values();
        } else {
            return Collections.emptySet();
        }
    }


    /**
     * Returns the cached session with the corresponding {@code sessionId} or {@code null} if there is
     * no session cached under that id (or if there is no Cache).
     *
     * @param sessionId the id of the cached session to acquire.
     * @return the cached session with the corresponding {@code sessionId}, or {@code null} if the session
     *         does not exist or is not cached.
     */
    private Session getCachedSession(Serializable sessionId) {
        Session cached = null;
        if (sessionId != null) {
            Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
            if (cache != null) {
                cached = cache.get(sessionId);
            }
        }
        return cached;
    }


    /**
     * Removes the specified Session from the cache.
     *
     * @param session the session to remove from the cache.
     */
    private void uncache(Session session) {
        if (session == null) {
            return;
        }
        Serializable id = session.getId();
        if (id == null) {
            return;
        }
        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
        if (cache != null) {
            cache.remove(id);
        }
    }

    /**
     * Caches the specified session under the cache entry key of {@code sessionId}.
     *
     * @param session   the session to cache
     * @param sessionId the session id, to be used as the cache entry key.
     * @since 1.0
     */
    private void cache(Session session, Serializable sessionId) {
        if (session == null || sessionId == null) {
            return;
        }
        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
        if (cache == null) {
            return;
        }
        cache.put(sessionId, session);
    }
    
    /**
     * Attempts to acquire the Session from the cache first using the session ID as the cache key.  If no session
     * is found, {@code super.readSession(sessionId)} is called to perform the actual retrieval.
     *
     * @param sessionId the id of the session to retrieve from the EIS.
     * @return the session identified by {@code sessionId} in the EIS.
     * @throws UnknownSessionException if the id specified does not correspond to any session in the cache or EIS.
     */
    @Override
    // 此方法线程同步，防止多个线程同时更新cache
    public synchronized Session readSession(Serializable sessionId) {
        Session s = getCachedSession(sessionId);

        // 如果设置了从远端读取session的间隔时间，就去检查
        if (readFromRemote && needPullFromRemote(s)) {
            return updateSessionFromRemoteCache(s);
        }

        if (!weakHashSet.contains(sessionId)) {
            // 如果没有执行过doReadSession，则从远端更新数据
            s = doReadSession(sessionId);
            cache(s, sessionId);
            weakHashSet.add(sessionId);
        }

        return s;
    }
    
    // 判断是否需要从远端读取数据
    private boolean needPullFromRemote(Session s) {
        return s != null && ShiroSessionFactory.isLogged(s) 
                && ((ExtSimpleSession) s).reachUpdateTime();
    }
    
   
    private Session updateSessionFromRemoteCache(Session s) {
        logger.debug("间隔大于{}秒的访问，开始从远端更新session......", 
                ((ExtSimpleSession) s).getUpdateInterval());
        s = doReadSession(s.getId());
        // 将远程缓存更新到本地缓存中
        if (s != null) {
            ((SimpleSession) s).setLastAccessTime(new Date());
            ((ExtSimpleSession) s).setLastUpdateTime(System.currentTimeMillis());
            cache(s, s.getId());
        } else {
            // 远端session已经没了，可能是过期了或者被删除了，此时应该删除本地缓存。
            logger.info("远端session已经没了，可能是过期了或者被删除了。do delete local cache......");
            uncache(s);
        }
        return s;
    }
    
    /**
     * Calls {@code super.create(session)}, then caches the session keyed by the returned {@code sessionId}, and then
     * returns this {@code sessionId}.
     *
     * @param session Session object to create in the EIS and then cache.
     */
    @Override
    public Serializable create(Session session) {
        Serializable sessionId = super.create(session);
        cache(session, sessionId);
        return sessionId;
    }
    
    /**
     * Updates the state of the given session to the EIS by first delegating to
     * {@link #doUpdate(org.apache.shiro.session.Session)}.  If the session is a {@link ValidatingSession}, it will
     * be added to the cache only if it is {@link ValidatingSession#isValid()} and if invalid, will be removed from the
     * cache.  If it is not a {@code ValidatingSession} instance, it will be added to the cache in any event.
     *
     * @param session the session object to update in the EIS.
     * @throws UnknownSessionException if no existing EIS session record exists with the
     *                                 identifier of {@link Session#getId() session.getId()}
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
            delete(session);
        } else {
            doUpdate(session);
            cache(session, session.getId());
        }
    }

    /**
     * Removes the specified session from any cache and then permanently deletes the session from the EIS by
     * delegating to {@link #doDelete}.
     *
     * @param session the session to remove from caches and permanently delete from the EIS.
     */
    @Override
    public void delete(Session session) {
        uncache(session);
        doDelete(session);
    }
    

    
    /**
     * Subclass implementation hook to actually persist the {@code Session}'s state to the underlying EIS.
     *
     * @param session the session object whose state will be propagated to the EIS.
     */
    protected abstract void doUpdate(Session session);


    /**
     * Subclass implementation hook to permanently delete the given Session from the underlying EIS.
     *
     * @param session the session instance to permanently delete from the EIS.
     */
    protected abstract void doDelete(Session session);

    /**
     * @return the readFromRemote
     */
    public boolean isReadFromRemote() {
        return readFromRemote;
    }

    /**
     * <p>只要是有相同的session id，用户可以在多台服务器上任意切换，
     * 但在这些服务器其中一台上对session的更改或删除，不能够被其他服务器及时自动的读取到。
     * 甚至，当用户在一台服务器上主动退出时，无法通知其他服务器清除本地缓存，
     * 会导致用户切换到其他服务器上时，仍然是登录状态，除非他在登录过的每台服务器上都主动退出。
     * <p>所以，如果可能出现这种情况，建议设置一个“从远程cache中读取session的间隔时间”，
     * 超过间隔时间没更新的session，每台服务器都会重新从远端读取更新。
     * <p>如果这个参数为false（默认值），将只会从本地内存中读取session，不允许用户在多台服务器之间切换。
     */
    public void setReadFromRemote(boolean readFromRemote) {
        this.readFromRemote = readFromRemote;
    }

}
